import * as RAPIER from '@dimforge/rapier3d'
import * as THREE from 'three'

/*
  Manage physics related components
*/

class Physics {
    constructor() {
        this.gravity = new THREE.Vector3(0.0, -9.81, 0.0)

        // Initialize Rapier world
        this.world = new RAPIER.World(this.gravity)

        // this.mcc = new MyCharacterController(this.world)
        this.meshes = []
        this.meshMap = new WeakMap()
        this._vector = new THREE.Vector3()
        this._quaternion = new THREE.Quaternion()
        this._matrix = new THREE.Matrix4()

        // this.world.numSolverIterations = 4; // Default = 4
        // this.eventQueue = new EventQueue(true);
        // this.debugger = new Debugger(this.world);
        // this.entities = new Map();
        // this.jointQueue = [];
    }
    // static async create() {
    //     const RAPIER = await import('@dimforge/rapier3d')
    //     return new Physics(RAPIER)
    // }

    getShape(mesh) {
        // todo 为什么这个时候取不到matrixWorld,但是print能看到
        // 还没加到场景里,没有父级

        const { geometry, matrixWorld } = mesh

        // todo  有什么用？
        const { x, y, z } = threeScene.tempCenter
        console.log('🚀 ~ Physics ~ getShape ~ x, y, z:', x, y, z)

        const parameters = geometry.parameters

        if (geometry.type === 'BoxGeometry') {
            const sx =
                parameters.width !== undefined ? parameters.width / 2 : 0.5
            const sy =
                parameters.height !== undefined ? parameters.height / 2 : 0.5
            const sz =
                parameters.depth !== undefined ? parameters.depth / 2 : 0.5

            return RAPIER.ColliderDesc.cuboid(sx, sy, sz)
        } else if (
            geometry.type === 'SphereGeometry' ||
            geometry.type === 'IcosahedronGeometry'
        ) {
            const radius =
                parameters.radius !== undefined ? parameters.radius : 1
            return RAPIER.ColliderDesc.ball(radius)
        } else if (geometry.type === 'PlaneGeometry') {
            const radius =
                parameters.radius !== undefined ? parameters.radius : 1
            const height =
                parameters.height !== undefined ? parameters.height : 1
            // const segments =
            //     parameters.radialSegments !== undefined
            //         ? parameters.radialSegments
            //         : 8
            const segment = height / 2 - radius
            return RAPIER.ColliderDesc.capsule(segment, radius)
        } else if (geometry.type === 'BufferGeometry') {
            const vertices = []
            const vertex = new THREE.Vector3()
            const position = geometry.getAttribute('position')

            for (let i = 0; i < position.count; i++) {
                vertex.fromBufferAttribute(position, i)
                vertices.push(vertex.x - x, vertex.y - y, vertex.z - z)
            }

            // if the buffer is non-indexed, generate an index buffer
            const indices =
                geometry.getIndex() === null
                    ? Uint32Array.from(
                          Array(parseInt(vertices.length / 3)).keys()
                      )
                    : geometry.getIndex().array

            return RAPIER.ColliderDesc.trimesh(vertices, indices)
        }

        return null
    }

    addScene(object3D) {
        object3D.traverse((child) => {
            if (child.isMesh) {
                const physics = child.userData.physics
                if (physics) {
                    this.addMesh(child, physics.mass, physics.restitution)
                }
            }
        })
    }

    addMesh(mesh, mass = 0, restitution = 0) {
        const { meshes, meshMap } = this
        const shape = this.getShape(mesh)

        if (shape === null) return

        shape.setMass(mass)
        shape.setRestitution(restitution)

        const body = mesh.isInstancedMesh
            ? this.createInstancedBody(mesh, mass, shape)
            : this.createBody(mesh.position, mesh.quaternion, mass, shape)

        if (mass > 0) {
            meshes.push(mesh)
            meshMap.set(mesh, body)
        }
    }

    createInstancedBody(mesh, mass, shape) {
        const { _vector } = this

        const array = mesh.instanceMatrix.array

        const bodies = []

        for (let i = 0; i < mesh.count; i++) {
            const position = _vector.fromArray(array, i * 16 + 12)
            bodies.push(this.createBody(position, null, mass, shape))
        }

        return bodies
    }

    createBody(position, quaternion, mass, shape) {
        const desc =
            mass > 0
                ? RAPIER.RigidBodyDesc.dynamic()
                : RAPIER.RigidBodyDesc.fixed()
        desc.setTranslation(...position)
        if (quaternion !== null) desc.setRotation(quaternion)

        const body = this.world.createRigidBody(desc)

        this.world.createCollider(shape, body)
        return body
    }

    setMeshPosition(mesh, position, index = 0) {
        const { meshMap } = this
        let body = meshMap.get(mesh)

        if (mesh.isInstancedMesh) {
            body = body[index]
        }

        body.setAngvel(ZERO)
        body.setLinvel(ZERO)
        body.setTranslation(position)
    }

    setMeshVelocity(mesh, velocity, index = 0) {
        const { meshMap } = this
        let body = meshMap.get(mesh)

        if (mesh.isInstancedMesh) {
            body = body[index]
        }

        body.setLinvel(velocity)
    }
}

class MyCharacterController {
    constructor(physics) {
        this.physics = physics
        // this.world = physics.world
        this.clock = new THREE.Clock()
        this.rigidBody = this.createRigid({ x: 1, y: 0.99, z: 1 })
        this.collider = this.createCollider(this.rigidBody, {
            x: 0,
            y: 0,
            z: 0,
        })
        this.characterController = this.createCharacterController()
        this.nextTranslation = new THREE.Vector3()
    }

    createRigid(translation) {
        const { world } = this.physics

        const rigidBodyDesc =
            RAPIER.RigidBodyDesc.kinematicPositionBased().setTranslation(
                translation.x,
                translation.y,
                translation.z
            )
        // .setTranslation(translation)
        // .setCcdEnabled(true)
        // .setSoftCcdPrediction(0.5)
        // .setGravityScale(1)

        // All done, actually build the rigid-body.
        const rigidBody = world.createRigidBody(rigidBodyDesc)
        // rigidBody.setGravityScale(0)
        // The integer handle of the rigid-body can be read from the `handle` field.
        // const rigidBodyHandle = rigidBody.handle

        return rigidBody
    }

    createCollider(rigidBody, translation = { x: 0, y: 0, z: 0 }) {
        const { world } = this.physics
        const colliderDesc = RAPIER.ColliderDesc.capsule(0.7, 0.2)
            // The collider translation wrt. the body it is attached to.
            // Default: the zero vector.
            .setTranslation(translation.x, translation.y, translation.z)
        // The collider rotation wrt. the body it is attached to, as a unit quaternion.
        // Default: the identity rotation.
        // .setRotation({ w: 1.0, x: 0.0, y: 0.0, z: 0.0 })
        // The collider density. If non-zero the collider's mass and angular inertia will be added
        // to the inertial properties of the body it is attached to.
        // Default: 1.0
        // .setDensity(1.3)
        // The friction coefficient of this collider.
        // Default: 0.5
        // .setFriction(0.0)
        // Whether this collider is a sensor.
        // Default: false
        // .setSensor(true)

        // Or create the collider and attach it to a rigid-body.
        const collider = world.createCollider(colliderDesc, rigidBody)
        return collider
    }

    createCharacterController() {
        // Create character controller from world

        const { world } = this.physics
        const controller = world.createCharacterController(0.01) // Spacing

        // Update controller settings
        controller.setSlideEnabled(true) // Allow sliding down hill
        controller.setMaxSlopeClimbAngle((75 * Math.PI) / 180) // Don’t allow climbing slopes larger than 45 degrees.
        controller.setMinSlopeSlideAngle((45 * Math.PI) / 180) // Automatically slide down on slopes smaller than 30 degrees.
        controller.enableAutostep(0.5, 0.2, true) // (maxHeight, minWidth, includeDynamicBodies) Stair behavior
        // controller.enableSnapToGround(0.5) // (distance) Set ground snap behavior
        // controller.setApplyImpulsesToDynamicBodies(true) // Add push behavior
        controller.setCharacterMass(65) // (mass) Set character mass
        return controller
    }

    testCollision(desiredTranslation, callback) {
        // todo 这个timestamp 有什么用吗？

        const { world } = this.physics
        // this.world.timestep = this.clock.getDelta()
        world.step()

        const { characterController, collider } = this
        characterController.computeColliderMovement(
            collider, // The collider we would like to move.
            desiredTranslation // The movement we would like to apply if there wasn’t any obstacle.
        )

        const translation = this.rigidBody.translation()
        const computedMovement = this.characterController.computedMovement()

        this.nextTranslation.copy(translation)
        this.nextTranslation.add(computedMovement)
        this.rigidBody.setNextKinematicTranslation(this.nextTranslation)

        callback(this.nextTranslation)

        for (let i = 0; i < characterController.numComputedCollisions(); i++) {
            let collision = characterController.computedCollision(i)
            // console.log('ok', collision)

            // Do something with that collision information.
        }
    }
}

export { Physics, MyCharacterController }
